/**
*
Java Diagram Package; An extremely flexible and fast multipurpose diagram
component for Swing.
Copyright (C) 2001 Eric Crahen <crahen@cse.buffalo.edu>
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
package diagram;
import java.awt.Color;
import java.awt.Component;
import java.awt.Graphics;
import java.awt.Rectangle;
import java.awt.event.KeyEvent;
import java.awt.geom.Point2D;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import javax.swing.CellRendererPane;
import javax.swing.InputMap;
import javax.swing.JComponent;
import javax.swing.KeyStroke;
import javax.swing.RepaintManager;
import javax.swing.UIManager;
import javax.swing.border.Border;
import javax.swing.plaf.ComponentUI;
import diagram.figures.PolyLink;
/**
* @class DiagramUI
*
* @date 08-20-2001
* @author Eric Crahen
* @version 1.0
*
* This is the based UI delegate for a Diagram component.
*/
public class DiagramUI extends ComponentUI {
static { // Install the UI with the UIManager
UIManager.put("diagram.background", new Color(0xEF, 0xEF, 0xEF));
UIManager.put("diagram.foreground", Color.black);
}
// Associated components
protected Diagram diagram;
protected boolean fastRefresh;
protected CellRendererPane cellRendererPane = new CellRendererPane();
protected ModelHandler modelListener = new ModelHandler();
protected PropertyChangeHandler propertyListener = new PropertyChangeHandler();
// Layer for sorting the figures
protected Layer figureLayer = new Layer();
protected Layer linkLayer = new Layer();
// Rectangles cached for bounds calculations
protected Rectangle clip = new Rectangle();
protected Rectangle bounds = new Rectangle();
// Caches for editors and renderers
private static HashMap editorCache = new HashMap();
private static HashMap rendererCache = new HashMap();
// Map for links
protected HashMap linkMap = new HashMap();
// Orginal Color & Border
private Color originalForeground;
private Color originalBackground;
/**
*
*/
public DiagramUI() {
UIManager.getDefaults().addPropertyChangeListener(propertyListener);
}
/**
* Get a FigureRenderer from the cache, if a renderer of that class does
* not yet exist, create and cache on using the default constructor.
*
* @return FigureRenderer
*/
public synchronized static FigureRenderer getRenderer(Class c) {
FigureRenderer renderer = (FigureRenderer)rendererCache.get(c);
if(renderer == null) {
try {
renderer = (FigureRenderer)c.newInstance();
rendererCache.put(c, renderer);
} catch(Throwable t) { /* ignore renderers that have no default constructor */ }
}
return renderer;
}
/**
* Get a FigureEditor from the cache, if a renderer of that class does
* not yet exist, create and cache on using the default constructor.
*
* @return FigureEditor
*/
public synchronized static FigureEditor getEditor(Class c) {
FigureEditor editor = (FigureEditor)editorCache.get(c);
if(editor == null) {
try {
editor = (FigureEditor)c.newInstance();
editorCache.put(c, editor);
} catch(Throwable t) { /* ignore editors that have no default constructor */ }
}
return editor;
}
/**
* Create a new UI for a Diagram
*
* @return ComponentUI
*/
public static ComponentUI createUI(JComponent c) {
return new DiagramUI();
}
/**
* Install this UI on a Diagram
*
* @param JComponent
*/
public void installUI(JComponent c) {
if(!(c instanceof Diagram))
throw new RuntimeException("This UI is for Diagram components only");
diagram = (Diagram)c;
// Install the listeners
diagram.add(cellRendererPane);
diagram.addPropertyChangeListener(propertyListener);
DiagramModel model = diagram.getModel();
if(model != null)
model.addDiagramDataListener(modelListener);
SelectionModel selectionModel = diagram.getSelectionModel();
if(selectionModel != null)
selectionModel.addSelectionListener(modelListener);
// Install the key mapping for editors
InputMap map = c.getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT);
map.put(KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0, false), "cancelEditing");
// Install the key mappings for the clipboard
map = diagram.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW);
// some keyboards have special keys for copy/cut/paste
map.put(KeyStroke.getKeyStroke(KeyEvent.VK_CUT, 0, false), "cut");
map.put(KeyStroke.getKeyStroke(KeyEvent.VK_COPY, 0, false), "copy");
map.put(KeyStroke.getKeyStroke(KeyEvent.VK_PASTE, 0, false), "paste");
// add mappings for the usualy copy/cut/paste keystokes as well
map.put(KeyStroke.getKeyStroke(KeyEvent.VK_DELETE, 0, false), "cut");
map.put(KeyStroke.getKeyStroke("control X"), "cut");
map.put(KeyStroke.getKeyStroke("control C"), "copy");
map.put(KeyStroke.getKeyStroke("control INSERT"), "copy");
map.put(KeyStroke.getKeyStroke("control V"), "paste");
map.put(KeyStroke.getKeyStroke("shift INSERT"), "paste");
installRenderers(diagram);
installEditors(diagram);
// Change the colors
installColors(diagram);
}
/**
* Install theme for the diagram under this UI
*
* @param Diagram
*/
protected void installColors(Diagram diagram) {
originalForeground = diagram.getForeground();
originalBackground = diagram.getBackground();
diagram.setBackground(UIManager.getColor("diagram.background"));
diagram.setForeground(UIManager.getColor("diagram.foreground"));
}
/**
* Install editors for the diagram under this UI
*
* @param Diagram
*/
protected void installRenderers(Diagram diagram) {
diagram.setFigureRenderer(Object.class, getRenderer(DefaultFigureRenderer.class));
diagram.setFigureRenderer(PolyLink.class, getRenderer(DefaultLinkRenderer.class));
}
/**
* Install renderer for the diagram under this UI
*
* @param Diagram
*/
protected void installEditors(Diagram diagram) {
diagram.setFigureEditor(Object.class, getEditor(DefaultFigureEditor.class));
diagram.setFigureEditor(PolyLink.class, getEditor(DefaultLinkEditor.class));
}
/**
* Uninstall this UI from a Diagram
*
* @param JComponent
*/
public void uninstallUI(JComponent c) {
if(!(c instanceof Diagram))
throw new RuntimeException("This UI is for Diagram components only");
if(c != diagram)
throw new RuntimeException("This UI is not installed on this Diagram");
// Remove the listeners
diagram.remove(cellRendererPane);
diagram.removePropertyChangeListener(propertyListener);
DiagramModel model = diagram.getModel();
if(model != null)
model.removeDiagramDataListener(modelListener);
SelectionModel selectionModel = diagram.getSelectionModel();
if(selectionModel != null)
selectionModel.removeSelectionListener(modelListener);
// Remove the key mapping for editors
InputMap map = c.getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT);
map.remove(KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0, false));
// Remove the key mappings for the clipboard
map = diagram.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW);
// some keyboards have special keys for copy/cut/paste
map.remove(KeyStroke.getKeyStroke(KeyEvent.VK_CUT, 0, false));
map.remove(KeyStroke.getKeyStroke(KeyEvent.VK_COPY, 0, false));
map.remove(KeyStroke.getKeyStroke(KeyEvent.VK_PASTE, 0, false));
// remove mappings for the usualy copy/cut/paste keystokes as well
map.remove(KeyStroke.getKeyStroke(KeyEvent.VK_DELETE, 0, false));
map.remove(KeyStroke.getKeyStroke("control X"));
map.remove(KeyStroke.getKeyStroke("control C"));
map.remove(KeyStroke.getKeyStroke("control INSERT"));
map.remove(KeyStroke.getKeyStroke("control V"));
map.remove(KeyStroke.getKeyStroke("shift INSERT"));
// Uninstall colors
uninstallColors(diagram);
// Clear any reused objects that need clearing
diagram = null;
linkLayer.removeAll();
figureLayer.removeAll();
}
/**
* Install theme for the diagram under this UI
*
* @param Diagram
*/
protected void uninstallColors(Diagram diagram) {
diagram.setBackground(originalBackground);
diagram.setForeground(originalForeground);
}
/**
* Find the Figure at the given point.
*
* @param Point2D
*
* @return Figure or null
*/
public Figure findFigure(Point2D pt) {
Figure figure = figureLayer.findFigure(pt);
if(figure == null)
figure = linkLayer.findFigure(pt);
return figure;
}
/**
* Paint the associated Diagram component
*
* @param Graphics
*/
public void paint(Graphics g, JComponent c) {
linkLayer.paintLayer(g);
figureLayer.paintLayer(g);
}
/**
* Refresh a Figure by using the decorated area recommended by the renderer
* that paints that figure. this either damages or repaints depending on wether
* or not fast refresh is enabled.
*
* @param Figure
*/
public void refreshFigure(Figure figure) {
if(fastRefresh)
repaintFigure(figure);
else
damageFigure(figure);
}
/**
* Refresh a region
*
* @param Rectangle - region to update or null for whole diagram
*/
public void refreshRegion(Rectangle rc) {
if(fastRefresh)
repaintRegion(rc);
else
damageRegion(rc);
}
/**
* Repaint an area of the associated Diagram component
*
* @param Rectangle - region to update or null for whole diagram
*/
public void repaintRegion(Rectangle rc) {
RepaintManager m = RepaintManager.currentManager(diagram);
if(rc != null)
m.addDirtyRegion(diagram, rc.x, rc.y, rc.width, rc.height);
else
m.markCompletelyDirty(diagram);
m.paintDirtyRegions();
}
/**
* Repaint a Figure by using the decorated area recommended by the renderer
* that paints that figure
*
* @param Figure
*/
public void repaintFigure(Figure figure) {
FigureRenderer renderer = diagram.getFigureRenderer(figure.getClass());
bounds = (Rectangle)renderer.getDecoratedBounds(diagram, figure, bounds);
repaintRegion(bounds);
}
/**
* Damage an area of the associated Diagram component
*
* @param Rectangle - region to update or null for whole diagram
*/
public void damageRegion(Rectangle rc) {
RepaintManager m = RepaintManager.currentManager(diagram);
if(rc == null)
m.markCompletelyDirty(diagram);
else
m.addDirtyRegion(diagram, rc.x, rc.y, rc.width, rc.height);
}
/**
* Damage a Figure by using the decorated area recommended by the renderer
* that paints that figure
*
* @param Figure
*/
public void damageFigure(Figure figure) {
// Damage the figure
FigureRenderer renderer = diagram.getFigureRenderer(figure.getClass());
bounds = (Rectangle)renderer.getDecoratedBounds(diagram, figure, bounds);
damageRegion(bounds);
}
/**
* Create an association between a link & figure so that the link
* can be repainted with the figure.
*/
public void addConnection(Figure figure, Link link) {
// Map the figure to the link
ArrayList list = (ArrayList)linkMap.get(figure);
if(list == null) {
list = new ArrayList();
linkMap.put(figure, list);
}
list.add(link);
}
/**
* Break an association between a link & figure so that the link
* can no longer be repainted with the figure.
*/
public void removeConnection(Figure figure, Link link) {
ArrayList list = (ArrayList)linkMap.get(figure);
if(list != null) {
list.remove(link);
if(list.isEmpty())
linkMap.remove(figure);
}
}
/**
* Get the null terminated list of Figures connected with a specific figure
*
* @param Figure
* @param Figure[] reuse
* @return Figure[]
*/
public Figure[] getConnected(Figure figure, Figure[] array) {
// Get the maped figures
ArrayList list = (ArrayList)linkMap.get(figure);
if(list != null) {
// Allocate an array if needed
if(array == null)
array = new Figure[list.size()];
array = (Figure[])list.toArray((Object[])array);
} else if(array != null && list != null && list.size() > 0)
list.set(0, null); // Terminate array
return array;
}
/**
* @class Layer
*
* A Layer keeps track of the z-order for items in a Diagram. The z-order
* in which the figures are displayed is not part of the data model, its
* a responibiliy of the view of that data model.
*/
protected class Layer {
// List to keep track of items in the layer
protected ArrayList figureList = new ArrayList();
/**
* Add a figure to this Layer
*/
public void add(Figure f) {
figureList.add(f);
}
/**
* Remove all Figures associated with this Layer
*/
public void removeAll() {
figureList.clear();
}
/**
* Remove a Figure from this Layer
*/
public void remove(Figure f) {
figureList.remove(f);
}
/**
* Move a Figure to the top of the list (higher z-order)
*
* @param Figure
*/
public void raise(Figure f) {
// Pop it out of the list and Push it back into the back
figureList.remove(f);
figureList.add(f);
}
/**
* Move a Figure to the top of the list (lower z-order)
*
* @param Figure
*/
public void lower(Figure f) {
// Pop it out of the list and Push it back into the front
figureList.remove(f);
figureList.add(0, f);
}
/**
* Test the layer to see if it contains a specific Figure
*
* @param Figure
*/
public boolean contains(Figure f) {
return figureList.contains(f);
}
/**
* Find the Figure at the given point. This searches for the higest
* z-order match.
*
* @param Object if an instance of Class is provided only a result of
* that type can be returned.
*
* @return Figure
*/
public Figure findFigure(Point2D pt) {
// Walk the list backwards to
for(int i = figureList.size(); --i >= 0;) {
Figure f = (Figure)figureList.get(i);
if(f.contains(pt))
return f;
}
return null;
}
/**
* Paint the layer on some Diagram. Subclasses can override the
* paintComponet method to change how this layer is painted.
*
* @param Graphics context to paint on
*/
public void paintLayer(Graphics g) {
clip = g.getClipBounds(clip);
SelectionModel selectionModel = diagram.getSelectionModel();
for(int i=0; i < figureList.size(); i++) {
// Get each Figure and paint it with the renderer the diagram supplies.
Figure figure = (Figure)figureList.get(i);
FigureRenderer renderer = diagram.getFigureRenderer(figure.getClass());
if(renderer == null)
throw new RuntimeException("No renderer for this Figure");
// Draw the item if it intersects the clipping rectangle
if(clip == null || figure.intersects(clip)) {
// Figure out if this is a selected component
boolean hasFocus = (selectionModel == null) ? false : selectionModel.contains(figure);
// Get the Component that should be used to render this Figure
Component c = renderer.getRendererComponent(diagram, figure, hasFocus);
if(c == null)
throw new RuntimeException("No renderer Component for this Figure");
// Set the bounds of the Component to match its counterpart (Figure)
bounds = (Rectangle)renderer.getDecoratedBounds(diagram, figure, bounds);
// Paint the figure
paintFigure(g, c, bounds);
}
}
}
/**
* Paint the figure on the Diagram with the given rendering Component
*
* @param Graphics
* @param Component
* @param Rectangle
*/
public void paintFigure(Graphics g, Component c, Rectangle r) {
cellRendererPane.paintComponent(g, c, diagram, r.x, r.y, r.width, r.height, true);
}
} /* Layer */
/**
* @class ModelHandler
*
* Listens to the DiagramModel for Figures being added, removed, selected and
* deselected.
*/
protected class ModelHandler
implements DiagramModelListener, DiagramSelectionListener {
/**
* Notify of a new figure being added to a DiagramModel
*
* @param DiagramModel
* @param Figure
*/
public void figureAdded(DiagramModel model, Figure figure) {
if(figure instanceof Link) {
Link link = (Link)figure;
linkLayer.add(link);
addConnection(link.getSource(), link);
addConnection(link.getSink(), link);
} else
figureLayer.add(figure);
}
/**
* Notify of a figure being removed from a DiagramModel
*
* @param DiagramModel
* @param Figure
*/
public void figureRemoved(DiagramModel model, Figure figure) {
if(figure instanceof Link) {
Link link = (Link)figure;
linkLayer.remove(link);
removeConnection(link.getSource(), link);
removeConnection(link.getSink(), link);
} else
figureLayer.remove(figure);
}
/**
* Figure selected
*/
public void figureAdded(SelectionModel model, Figure figure) {
if((figure instanceof Link) && linkLayer.contains(figure))
linkLayer.raise(figure);
else if(figureLayer.contains(figure))
figureLayer.raise(figure);
}
/**
* Figure deselected
*/
public void figureRemoved(SelectionModel model, Figure figure) { }
} /* ModelListener */
/**
* @class PropertyChangeHandler
*
*/
protected class PropertyChangeHandler implements PropertyChangeListener {
public void propertyChange(PropertyChangeEvent e) {
String propertyName = e.getPropertyName();
// Listen for DiagramModel changes.
if(propertyName.equals("model")) {
DiagramModel oldModel = (DiagramModel)e.getOldValue();
DiagramModel newModel = (DiagramModel)e.getNewValue();
if(oldModel != null)
oldModel.removeDiagramDataListener(modelListener);
linkLayer.removeAll();
figureLayer.removeAll();
linkMap.clear();
// Update the layers for the new model
if(newModel != null) {
newModel.addDiagramDataListener(modelListener);
for(Iterator i = newModel.iterator();i.hasNext();)
modelListener.figureAdded(newModel, (Figure)i.next());
}
// redraw
repaintDiagram();
} else if(propertyName.equals("selectionModel")) {
SelectionModel oldModel = (SelectionModel)e.getOldValue();
SelectionModel newModel = (SelectionModel)e.getNewValue();
if(oldModel != null)
oldModel.removeSelectionListener(modelListener);
if (newModel != null)
newModel.addSelectionListener(modelListener);
repaintDiagram();
} else if(propertyName.equals("fastRefresh")) {
fastRefresh = e.getNewValue().equals("true");
} else if(propertyName.equals("diagram.background")) {
diagram.setBackground((Color)e.getNewValue());
} else if(propertyName.equals("diagram.foreground")) {
diagram.setForeground((Color)e.getNewValue());
} else if(propertyName.equals("diagram.border")) {
diagram.setBorder((Border)e.getNewValue());
}
}
} /* PropertyChangeHandler */
/**
* Force a repaint
*/
private final void repaintDiagram() {
diagram.invalidate();
diagram.repaint();
}
}